<?php
/*
* >--- iNF0 ----<
* 
* SCRiPT: LiTTLE xSS F!LTER
* VERSiON: .6
* LiCENSE: GPLv3
* AUTH0R: Eve Hicks @ Alternate Des!gn (eve.hicks@gmail.com)
* THANKS T0:
*  Alexey "Deimos" Tkachenko (deimos@belastro.net) - Pair tags parser
*  P| (?) - 1 vulnerability (17-10-2009)
*  
* >--- L0G -----<
*
* Latest version notice:
*  ~ Fix - non-latin characters encode
*
* >--- N0TES ---<
*
* Allowed tags:
*  P, B, U, I, STRONG, EM, SUP, SUB, UL, LI, BR, S, SMALL, HR, SPAN, H1, H2, H3, H4, H5, H6
* Tag attributes:
*  IMG [src,alt,hspace,vspace,width,height,align]
*  A [href,title]
* BBCodes:
*  [URL=http://www.google.com]Google[/URL]
*  [IMG]http://www.google.com/logo.jpg[/IMG]
*
*/

class lixf {
        
	function lixf(){
		return true;
	}

	function filter($msg=""){

	$text = trim($msg);
	if(!empty($text)){
	// make all elements to entities
	$text = html_entity_decode($text);
	// decode %xx elements
	$text = rawurldecode($text);
	// remove slashes
	$text = stripslashes($text);
	// parse all tags to LiXF Code
	$text = str_replace(array('<','>'),array('{','}'),$text);
	$a = array(
// Simple tags
'/\{p\}/i','/\{\/p\}/i','/\{b\}/i','/\{\/b\}/i','/\{strong\}/i','/\{\/strong\}/i',
'/\{u\}/i','/\{\/u\}/i','/\{s\}/i','/\{\/s\}/i','/\{i\}/i','/\{\/i\}/i','/\{em\}/i','/\{\/em\}/i',
'/\{sup\}/i','/\{\/sup\}/i','/\{sub\}/i','/\{\/sub\}/i',
'/\{ul\}/i','/\{\/ul\}/i','/\{li\}/i','/\{\/li\}/i','/\{br\s?\/?\}/i','/\{hr\s?\/?\}/i',
'/\{small\}/i','/\{\/small\}/i','/\{span\}/i','/\{\/span\}/i',
'/\{h1\}/i','/\{\/h1\}/i','/\{h2\}/i','/\{\/h2\}/i','/\{h3\}/i','/\{\/h3\}/i','/\{h4\}/i','/\{\/h4\}/i','/\{h5\}/i','/\{\/h5\}/i','/\{h6\}/i','/\{\/h6\}/i',
// Attributes tags
'/\{a href=\"(http:\/\/|ftp:\/\/)([^\}\'\"]+)\"( title=\"[^\}\'\"]+\")?\}/i','/\{\/a\}/i',
'/\{img src=\"(http:\/\/|ftp:\/\/)([^\}\'\"]+)\"( alt=\"[^\}\'\"]+\")?( hspace=\"\d+\")?( vspace=\"\d+\")?( width=\"\d+\")?( height=\"\d+\")?( align=\"[^\}\'\"]+\")? \/\}/i',
// BBCode
'/\[URL=(http:\/\/|ftp:\/\/)([^\]\'\"]+)\]/i','/\[\/URL\]/i',
'/\[IMG\](http:\/\/|ftp:\/\/)([^\]\'\"]+)\[\/IMG\]/i'
	);
	$b = array(
// Simple Tags
'<p>','</p>','<b>','</b>','<strong>','</strong>',
'<u>','</u>','<s>','</s>','<i>','</i>','<em>','</em>',
'<sup>','</sup>','<sub>','</sub>',
'<ul>','</ul>','<li>','</li>','<br />','<hr />',
'<small>','</small>','<span>','</span>',
'<h1>','</h1>','<h2>','</h2>','<h3>','</h3>','<h4>','</h4>','<h5>','</h5>','<h6>','</h6>',
// Attributes tags
'<a href="\\1\\2"\\3 target="_blank" rel="nofollow">','</a>',
'<img src="\\1\\2"\\3\\4\\5\\6\\7\\8 />',
// BBCode
'<a href="\\1\\2\" target="_blank" rel="nofollow">','</a>',
'<img src="\\1\\2" />'
	);
	$text = preg_replace($a,$b,$text);
	// Clean Other { } to something safe
	$text = preg_replace("/(\{|\})/","*",$text);
	// Close all pair tags and etc.
	$text = $this->InsertPairTags($text,array($this,'LittleXSSFilter'));
	// Encode non-latin characters
	$text = nl2br(htmlspecialchars_decode($text));

		return $text;
	} else {
		return false;
		}
	} // func end
	
/**
 * Inserts pairs for tags in given string.
 * Copyright (c) 2009 Alexey Tkachenko
 * deimos@belastro.net
 * The source code of this function is distrubuted under GPLv3 license.
 *
 * @param string $html - HTML to process
 * @param [optional] callback $validateTag - function to validate each tag.<br />
 * Prototype of this function is <br />
 * [string|bool] function MatchCallback(&#36;tagName, &#36;fullTag)	<br />
 * &#36;tagName - parsed tag name<br />
 * &#36;fullTag - full content of parsed start tag<br />
 * For example, when tag &lt;a href="http://localhost/"&gt; is captured, $tagName will be 'a', and <br />
 * &#36;fullTag will be '&lt;a href="http://localhost/"&gt;' <br />
 * The return value of this callback is boolean or string. <br />
 * When callback returns true, this tag will be put into the output stream with no changes, <br />
 * when false - this tag will be completely removed.<br />
 * When callback returns string value, this value will be used to replace tag. <br />
 * You may implement some tag filtering, e.g. removing/adding attributes, uppercasing/lowercasing tag name etc.<br />
 * @param [optional]bool $tagsCaseSensitive - if true, parser will not ignore tag names case.
 * @return string
 */
function InsertPairTags($html, $validateTag = null, $tagsCaseSensitive = false){

	static $nonPairedTags = array('img','hr','br','meta','area','base','col','frame','input','link','param');

	static $pattern = '#<(/)?([-\w:]+)\s*(.*?)\s*(/)?>#isx';

	$splits = array_map('htmlspecialchars', preg_split($pattern, $html));
	preg_match_all($pattern, $html, $matches);

	$count = count($splits)-1;
	$tags = array();
	$output = array();

	for($i=0;$i<$count;++$i){
		$output[] = $splits[$i];

		$line = $matches[0][$i];
		if(empty($matches[1][$i]) && empty($matches[4][$i])){
			if(in_array(strtolower($matches[2][$i]), $nonPairedTags)){
				$line = str_replace('>', '/>', $line);
			}
		}
		preg_match($pattern, $line, $match);

		if(empty($match[1])){ // start tag

			$replacement = $validateTag ? call_user_func($validateTag, $match[2], $match[0]) : $match[0];

			if($replacement === true){
				$replacement = $match[0];
			} elseif ($replacement === false){
				continue;
			}

			preg_match($pattern, $replacement, $replacementMatch);
			if(empty($replacementMatch[1]) && empty($replacementMatch[4])){
				if(in_array(strtolower($replacementMatch[2]), $nonPairedTags, true)){
					$replacement = str_replace('>', '/>', $replacement);
				}
			}
			preg_match($pattern, $replacement, $replacementMatch);

			if(empty($replacementMatch[4])){ // tag is paired
				array_push($tags, $replacementMatch[2]);
			}

			$output[] = $replacementMatch[0];
			
		} else { // end tag

			if(count($tags) > 0){
				if($tagsCaseSensitive ? in_array($match[2], $tags, true) : in_array(strtolower($match[2]), array_map('strtolower', $tags), true)){
					do {
						$topTag = array_pop($tags);
						$output[] = "</$topTag>";

						if(count($tags) == 0){
							break;
						}
						$continueIterate = $tagsCaseSensitive ? $topTag === $match[2] : (strtolower($topTag) !== strtolower($match[2]));
					} while ($continueIterate);
				}
			}
		}
	}

	$output[] = $splits[$count];

	while(count($tags) > 0){
		$topTag = array_pop($tags);
		$output[] = "</$topTag>";
	}

	return implode($output);
}
	
function GetTagAttributes($tag, $allowed = null){
	$output = array();
	preg_match_all('#\b(\w+)=("(?:.+?)"|\'(?:.+?)\'|(?:\S+))#', $tag, $matches);
	foreach($matches[0] as $index => $v){
		$key = strtolower($matches[1][$index]);
		$value = trim($matches[2][$index], '"\'');
		if($allowed === null){
			$output[$key] = $value;
		} elseif (is_array($allowed)){
			if(in_array($key, $allowed, true)){
				$output[$key] = $value;
			}
		} else {
			// Uncomment this if you need debug
			//trigger_error('List of allowed tags must be an array');
			return false;
		}
	}
	return $output;
}

function LittleXSSFilter($tagName, $fullTag){
	static $allowedAttributes = array(
		'img' => array('src','alt','hspace','vspace','width','height','align'),
		'a' => array('href','title','target','rel'));

	switch($tagName = strtolower($tagName)){
		case 'p':
		case 'b': case 'strong':
		case 'i': case 'em':
		case 'u':
		case 'sup': case 'sub':
		case 'ul': case 'li':
		case 'small':
		case 'span':
		case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
			return "<$tagName>";
		case 'br':
		case 'hr':
			return "<$tagName />";
		case 'img':
		case 'a':
			$attrs = $this->GetTagAttributes($fullTag, $allowedAttributes[$tagName]);
			$outAttrs = "";
			foreach($attrs as $attr => $value){
				$outAttrs .= "$attr=\"$value\" ";
			}
			return "<$tagName $outAttrs>";
		default:
			return false;
	}
}

} // class end

?>